#pragma once

#include <string>
#include <vector>
#include "../database.hpp"
#include "../except.hpp"
#include "medicine.hpp"
#include "recordmodel.hpp"

namespace pharmacy_server
{
    class MedicineModel
    {
    public:
        static MedicineModel& instance()
        {
            static MedicineModel inst;
            return inst;
        }

    private:
        MedicineModel()
            : recordmodel(SalesRecordModel::instance())
        {
            try
            {
                easymysql::result res = easymysql::executer(database::instance())(R"(
SELECT id, name, category, supplier, price, stock, threshold, validity FROM medicines
                    )");

                Medicine::tuple_of_medicine line;
                while (res.get<Medicine::tuple_of_medicine>(&line))
                {
                    Medicine* medicine = new Medicine(line);
                    _medicines.push_back(medicine);
                    _category2medicines[medicine->category].push_back(medicine);
                }
            }
            catch (const easymysql::except& e)
            {
                easylog::error("medicine model init failed exception: {}", e.what());
                throw model_except("medicine model init failed exception: " + std::string(e.what()));
            }
        }

        MedicineModel(const MedicineModel&) = delete;
        MedicineModel operator=(const MedicineModel&) = delete;

    public:
        ~MedicineModel()
        {
            for (auto* med : _medicines)
                delete med;
        }

        bool add_medicine(const std::string& name, const std::string& category, const std::string& supplier, float price, int stock, int threshold, int validity)
        {
            try
            {
                easymysql::executer(database::instance())(R"(
INSERT INTO medicines (name, category, supplier, price, stock, threshold, validity) VALUES ('{}', '{}', '{}', {}, {}, {}, {})
                    )", name, category, supplier, price, stock, threshold, validity);

                Medicine* medicine = new Medicine(database::instance().insert_id(), name, category, supplier, price, stock, threshold, validity);

                _medicines.push_back(medicine);
                _category2medicines[category].push_back(medicine);

                std::cout << "size: " << _category2medicines.size() << std::endl;
                std::cout << "name: " << _category2medicines[category][0]->name << std::endl;
            }
            catch (const easymysql::except& e)
            {
                easylog::error("create medicine failed exception: {}", e.what());
                throw model_except("create medicine failed exception: " + std::string(e.what()));
                return false;
            }

            return true;
        }

        bool delete_medicine(const std::string& name)
        {
            try
            {
                Medicine* medicine = find_medicine(name);

                easymysql::executer(database::instance())("DELETE FROM medicines WHERE id = {}", medicine->id);

                bool ok1 = false, ok2 = false;
                auto& medicines = _category2medicines[medicine->category];

                for (auto it = medicines.begin(); it != medicines.end(); ++it)
                {
                    if ((*it)->id == medicine->id)
                    {
                        medicines.erase(it);
                        ok1 = true;
                        break;
                    }
                }
                for (auto it = _medicines.begin(); it != _medicines.end(); ++it)
                {
                    if ((*it)->id == medicine->id)
                    {
                        delete *it;
                        _medicines.erase(it);
                        ok2 = true;
                        break;
                    }
                }

                if (!ok1 || !ok2)
                {
                    easylog::error("delete medicine not found");
                    throw model_except("delete medicine not found");
                    return false;
                }
            }
            catch (const easymysql::except& e)
            {
                easylog::error("delete medicine failed exception : {}", e.what());
                throw model_except("delete medicine failed exception : " + std::string(e.what()));
                return false;
            }
            return true;
        }

        bool update_medicine(int id, const std::string& name, const std::string& category, const std::string& supplier, float price, int stock, int threshold, int validity)
        {
            try
            {
                easymysql::executer(database::instance())(R"(
UPDATE medicines SET name = '{}', category = '{}', supplier = '{}', price = {}, stock = {}, threshold = {}, validity = {} WHERE id = {}
                    )", name, category, supplier, price, stock, threshold, validity, id);

                if (!database::instance().affected_rows())
                    throw easymysql::except("update medicine not found");

                bool ok = false;
                for (auto* medicine : _medicines)
                {
                    if (medicine->id == id)
                    {
                        medicine->name = name;
                        medicine->category = category;
                        medicine->supplier = supplier;
                        medicine->price = price;
                        medicine->stock = stock;
                        medicine->threshold = threshold;
                        medicine->validity = validity;
                        ok = true;
                        break;
                    }
                }
                if (!ok)
                {
                    easylog::error("medicine not found");
                    throw model_except("medicine not found");
                }
            }
            catch (const easymysql::except& e)
            {
                easylog::error("update medicine failed exception : {}", e.what());
                throw model_except("update medicine failed exception : " + std::string(e.what()));
                return false;
            }
            return true;
        }

        Medicine* query_medicine_by_name(const std::string& name)
        {
            return find_medicine(name);
        }

        std::vector<Medicine*> query_medicines_by_category(const std::string& category)
        {
            std::vector<Medicine*> medicines;

            auto it = _category2medicines.find(category);
            if (it != _category2medicines.end())
            {
                for (auto* medicine : it->second)
                {
                    if (!medicine)
                        throw model_except("query medicine error");
                    medicines.push_back(medicine);
                }
            }
            else
            {
                easylog::info("query medicine by category failed, unknown category: {}", category);
            }

            return medicines;
        }

        std::vector<Medicine*> query_medicines_by_keyword(const std::string& keyword)
        {
            std::vector<Medicine*> medicines;
            for (auto* medicine : _medicines)
            {
                if (medicine->name.find(keyword) != std::string::npos
                 || medicine->category.find(keyword) != std::string::npos
                 || medicine->supplier.find(keyword) != std::string::npos)
                {
                    medicines.push_back(medicine);
                }
            }

            return medicines;
        }

        void take_medicine(const std::string& username, const std::string& name, int quantity)
        {
            try
            {
                Medicine* medicine = find_medicine(name);

                if (medicine && medicine->stock >= quantity)
                {
                    easymysql::executer(database::instance())(R"(
UPDATE medicines SET stock = stock - {} WHERE name = '{}'
                        )", quantity, name);

                    if (!database::instance().affected_rows())
                    {
                        easylog::error("take medicine not found");
                        throw model_except("take medicine not found");
                    }

                    medicine->stock -= quantity;
                    recordmodel.add_sales_record(username, name, quantity, "out");
                }
                else
                {
                    easylog::error("not enough stock for medicine_name: {}", name);
                    throw model_except("not enough stock");
                }
            }
            catch (const easymysql::except& e)
            {
                easylog::error("take medicine failed exception : {}", e.what());
                throw model_except("take medicine failed exception : " + std::string(e.what()));
            }
        }

        void purchase_medicine(const std::string& name, int quantity)
        {
            try
            {
                Medicine* medicine = find_medicine(name);

                easymysql::executer(database::instance())(R"(
UPDATE medicines SET stock = stock + {} WHERE name = '{}'
                    )", quantity, name);

                if (!database::instance().affected_rows())
                {
                    easylog::error("medicine not found");
                    throw model_except("medicine not found");
                }

                medicine->stock += quantity;
            }
            catch (const easymysql::except& e)
            {
                easylog::error("take medicine failed exception : {}", e.what());
                throw model_except("take medicine failed exception : " + std::string(e.what()));
            }
        }

public:
        // No need to check the return value,
        // because it will throw exception if not found
        Medicine* find_medicine(const std::string& name)
        {
            for (auto* medicine : _medicines)
            {
                if (medicine->name == name)
                    return medicine;
            }

            throw model_except("find medicine not found");
        }

        Medicine* get_medicine(int id)
        {
            for (auto& medicine : _medicines)
            {
                if (medicine->id == id)
                    return medicine;
            }

            throw model_except("get medicine not found");
        }

        const std::vector<Medicine*>& medicines() const
        {
            return _medicines;
        }

    private:
        SalesRecordModel& recordmodel;
        std::vector<Medicine*> _medicines;
        std::unordered_map<std::string, std::vector<Medicine*>> _category2medicines;
    };
}